import type { Metadata } from 'next'; import { ScrollX } from '@/components/models/scroll-x'; import Link from 'next/link'; import { notFound, permanentRedirect } from 'next/navigation'; import { CompareTrayBar } from '@/components/compare/compare-tray-bar'; import { CompareButton } from '@/components/compare/compare-button'; import { ViewBeacon } from '@/components/layout/view-beacon'; import { OpennessChip } from '@/components/models/badges'; import { FamilyBenchmarkProgress, FamilyReleaseLanes } from '@/components/models/family-blocks'; import { MiniGraph } from '@/components/models/mini-graph'; import { Chip, EntityBadge, StatusBadge } from '@/components/ui/badges'; import { DataTable, Td, Th } from '@/components/ui/data-table'; import { EntityLink, QualityMark } from '@/components/ui/entity'; import { Hint } from '@/components/ui/hint'; import { Container, Note } from '@/components/ui/section'; import { EmptyState } from '@/components/ui/unavailable'; import { ApiError, apiD1, safe } from '@/lib/api'; import { fmtDate, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format'; import { routes, SITE_NAME, SITE_URL } from '@/lib/site'; import type { FamilyDetail } from '@/lib/types'; type Params = { params: Promise<{ slug: string }> }; export const revalidate = 600; async function load(slug: string): Promise { try { return await apiD1.family(slug, 300); } catch (e) { if (e instanceof ApiError && e.notFound) notFound(); throw e; } } function describe(f: FamilyDetail): string { const pmin = num(f.param_range?.min); const pmax = num(f.param_range?.max); const bits = [`${fmtInt(f.model_count)} canonical models`, f.first_release ? `released ${fmtDate(f.first_release)} → ${f.last_release ? fmtDate(f.last_release) : 'today'}` : null, pmin !== null ? `${fmtParams(pmin)}${pmax !== null && pmax !== pmin ? ` – ${fmtParams(pmax)}` : ''} parameters` : null, f.licenses?.length ? `licences ${f.licenses.map((l) => l.key).slice(0, 3).join(', ')}` : null, num(f.artifacts_count) ? `${fmtInt(f.artifacts_count)} artifacts` : null].filter(Boolean); return `${f.name}${f.organization ? ` by ${f.organization.name}` : ''} — ${bits.join('; ')}. Release timeline, members, lineage, licence mix and benchmark progress on ${SITE_NAME}.`.slice(0, 300); } export async function generateMetadata({ params }: Params): Promise { const { slug } = await params; const f = await safe(apiD1.family(slug, 5)); if (!f || f.slug !== slug) return { title: 'Family', robots: { index: false } }; const title = `${f.name} family — Releases, Members, Lineage & Benchmarks`; const description = describe(f); const canonical = routes.family(f.slug); return { title, description, alternates: { canonical }, openGraph: { title: `${title} | ${SITE_NAME}`, description, url: `${SITE_URL}${canonical}`, type: 'article', siteName: SITE_NAME }, twitter: { card: 'summary_large_image', title, description } }; } export default async function FamilyPage({ params }: Params) { const { slug } = await params; const f = await load(slug); if (f.slug && f.slug !== slug) permanentRedirect(routes.family(f.slug)); const bench = await safe(apiD1.benchmarks()); const benchNames: Record = {}; for (const b of bench?.items ?? []) benchNames[b.slug] = b.name; const canonical = routes.family(f.slug); const members = [...f.members].sort((a, b) => String(b.key_facts?.release_date ?? '').localeCompare(String(a.key_facts?.release_date ?? '')) || a.model.name.localeCompare(b.model.name)); const pmin = num(f.param_range?.min); const pmax = num(f.param_range?.max); const nodes = f.members.map((m) => ({ id: m.model.id, label: m.model.name, href: routes.entity(m.model), sub: num(m.model.attributes?.parameter_count) !== null ? fmtParams(m.model.attributes.parameter_count) : undefined })); const licTotal = f.licenses.reduce((n, l) => n + (num(l.models) ?? 0), 0); const ranks = Object.entries(f.benchmark_best ?? {}).sort((a, b) => a[1].rank - b[1].rank); const root = typeof f.summary?.attributes?.family_root === 'string' ? (f.summary.attributes.family_root as string) : null; const ld = { '@context': 'https://schema.org', '@type': 'CreativeWorkSeries', name: f.name, url: `${SITE_URL}${canonical}`, description: describe(f), creator: f.organization ? { '@type': 'Organization', name: f.organization.name } : undefined, hasPart: members.slice(0, 20).map((m) => ({ '@type': 'SoftwareApplication', name: m.model.name, url: `${SITE_URL}${routes.entity(m.model)}` })) }; return (